/**
 * Copyright (c) 2009  Collaborative Development Group, C.S. Dept., University of Bari
 *
 * All rights reserved. This program and the accompanying materials are made available under
 * the terms of the Eclipse Public License v1.0  which accompanies this distribution, and is 
 * available at http://www.eclipse.org/legal/epl-v10.html
 */
package it.uniba.di.cdg.penelope.ui.mvp.scope;

import static com.google.common.base.Preconditions.checkState;
import it.uniba.di.cdg.penelope.ui.annotation.Model;
import it.uniba.di.cdg.penelope.ui.annotation.PresentationModel;
import it.uniba.di.cdg.penelope.ui.annotation.Presenter;

import java.util.HashMap;
import java.util.Map;

import com.google.common.collect.Maps;
import com.google.inject.Key;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.Scope;

/**
 * Support View scope: presenter, presentation model and models have their lifecycle linked
 * to the view: 
 * <ul>
 * <li>when view is created, the linked scope is set up.</li>
 * <li>when view is disposed, the linked scope is destroyed.</li>
 * </ul>
 * 
 * This code is based on the example on <a href="http://code.google.com/docreader/#p=google-guice&s=google-guice&t=CustomScopes">Guice site</a>. 
 */
public class ViewScope implements Scope {

	private final Map<String,Map<Key<?>, Object>> values = new HashMap<String,Map<Key<?>, Object>>();

	public void enter( String viewScopeId ) {
		System.out.println( "Entering View Scope: " + viewScopeId );
		checkState( values.get( viewScopeId ) == null, "A scoping block is already in progress" );
		values.put( viewScopeId, Maps.<Key<?>, Object> newHashMap() );
	}

	public void exit( String viewScopeId ) {
		System.out.println( "Cleaning View Scope: " + viewScopeId );
		checkState( values.get( viewScopeId ) != null, "No scoping block in progress" );
		values.remove( viewScopeId );
	}

	/* (non-Javadoc)
	 * @see com.google.inject.Scope#scope(com.google.inject.Key, com.google.inject.Provider)
	 */
	public synchronized <T> Provider<T> scope( final Key<T> key, final Provider<T> unscoped ) {
		return new Provider<T>() {
			public T get() {
				Map<Key<?>, Object> scopedObjects = getScopedObjectMap( key );

				@SuppressWarnings( "unchecked" )
				T current = (T) scopedObjects.get( key );
				if (current == null && !scopedObjects.containsKey( key )) {
					current = unscoped.get();
					scopedObjects.put( key, current );
				}
				return current;
			}
		};
	}

	private <T> Map<Key<?>, Object> getScopedObjectMap( Key<T> key ) {
		String storyId = getMVPId( key );

		Map<Key<?>, Object> scopedObjects = values.get( storyId );
		if (scopedObjects == null) {
			throw new OutOfScopeException( "Cannot access " + key + " outside of a scoping block" );
		}
		return scopedObjects;
	}

	/**
	 * Returns a provider that always throws exception complaining that the
	 * object in question must be seeded before it can be injected.
	 * 
	 * @return typed provider
	 */
	@SuppressWarnings( { "unchecked" } )
	public static <T> Provider<T> seededKeyProvider() {
		return (Provider<T>) SEEDED_KEY_PROVIDER;
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "ViewScope";
	}
	
	private String getMVPId( Key<?> key ) {
		Class<?> type = key.getTypeLiteral().getRawType();
		String storyId = null;
		if (type.getAnnotation( Presenter.class ) != null) {
			storyId = type.getAnnotation( Presenter.class ).value();
		} else if (type.getAnnotation( PresentationModel.class ) != null) {
			storyId = type.getAnnotation( PresentationModel.class ).value();
		} else if (type.getAnnotation( Model.class ) != null) {
			storyId = type.getAnnotation( Model.class ).value();
		} else {
			throw new ViewScopeException( "@ViewScope components must be annotated with one of @Presenter, @Model or @PresentationModel" );
		}
		return storyId; 			
	}

	private static final Provider<Object> SEEDED_KEY_PROVIDER = new Provider<Object>() {
		public Object get() {
			throw new IllegalStateException( "If you got here then it means that"
					+ " your code asked for scoped object which should have been"
					+ " explicitly seeded in this scope by calling" + " SimpleScope.seed(), but was not." );
		}
	};

	public <T> void seed( Key<T> key, T value ) {
		Map<Key<?>, Object> scopedObjects = getScopedObjectMap( key );
		checkState( !scopedObjects.containsKey( key ), "A value for the key %s was "
				+ "already seeded in this scope. Old value: %s New value: %s", key, scopedObjects.get( key ), value );
		scopedObjects.put( key, value );
	}

	public <T> void seed( Class<T> clazz, T value ) {
		seed( Key.get( clazz ), value );
	}
}